/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.config.ascrpt;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.openislands.oi.config.argumentresolver.HeaderVariable;
import org.openislands.oi.error.NoHeaderException;
import org.openislands.oi.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Aspect
@Component
@SuppressWarnings("unused")
public class RequestBodyAspect {
    private static final Logger log = LoggerFactory.getLogger(RequestBodyAspect.class);

    /***
     * For handler aspect join point
     * Lod see {@link #loadHandler}
     */
    private static final List<RequestBodyHandler> handlerList = new ArrayList<>();

    /***
     * Intercept all methods at the name *Controller of all classes under the org.openislands.oi.*.http package.
     * And params include @RequestBody
     */
    @Pointcut("execution(public * org.openislands.oi.*.http.*Controller.*(..))")
    public void controller() {
    }

    @Before("controller()")
    public void controllerBefore(JoinPoint joinPoint) throws Exception {
        final Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        final Parameter[] parameters = method.getParameters();
        Object[] pointArgs = joinPoint.getArgs();

        for (int i = 0; i < parameters.length && i < pointArgs.length; i++) {
            // arg include @RequestBody
            final Parameter parameter = parameters[i];
            if (parameter.isAnnotationPresent(RequestBody.class)) {
                for (RequestBodyHandler bodyHandler : handlerList) {
                    bodyHandler.handle(joinPoint, pointArgs[i]);
                }
            }
        }
    }

    @PostConstruct
    public void loadHandler() {
        handlerList.add(new HeaderVariableRequestBodyHandler());
    }

    /***
     * Aspect handler
     */
    @FunctionalInterface
    public interface RequestBodyHandler {

        /***
         * Handle parameters annotated with @RequestBody in aspect
         * @param joinPoint     aspect join point
         * @param arg           arg with @RequestBody
         * @throws Exception    after throw an exception will be returned directly from the controller
         */
        void handle(JoinPoint joinPoint, Object arg) throws Exception;
    }

    /***
     * Process fields with {@link HeaderVariable} annotation in arg
     */
    private static class HeaderVariableRequestBodyHandler implements RequestBodyHandler {
        private final Map<Class<?>, List<Field>> matchFieldCache = new ConcurrentHashMap<>();

        @Override
        public void handle(JoinPoint joinPoint, Object arg) throws NoHeaderException {
            try {
                HttpServletRequest request;
                ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (Objects.isNull(attributes)) {
                    return;
                }

                request = attributes.getRequest();
                Class<?> aClass = arg.getClass();
                List<Field> matchFieldList = this.matchFieldCache.get(aClass);
                // add match cache. field include HeaderVariable and HeaderVariable.value no empty
                if (Objects.isNull(matchFieldList)) {
                    matchFieldList = new ArrayList<>();
                    Field[] fields = aClass.getDeclaredFields();
                    Arrays.stream(fields)
                            .filter(field -> {
                                HeaderVariable headerVariable;
                                return Objects.nonNull(headerVariable = field.getAnnotation(HeaderVariable.class))
                                        && StringUtils.noTrimEmpty(headerVariable.value());
                            })
                            .forEach(matchFieldList::add);
                    this.matchFieldCache.put(aClass, matchFieldList);
                }
                // set http header
                for (Field field : matchFieldList) {
                    HeaderVariable headerVariable = field.getAnnotation(HeaderVariable.class);
                    String value = headerVariable.value();
                    String header = request.getHeader(value);
                    if (Objects.isNull(header) && headerVariable.required()) {
                        throw new NoHeaderException("Http header " + value + " is required");
                    } else {
                        field.setAccessible(true);
                        field.set(arg, header);
                    }
                }
            } catch (ReflectiveOperationException e) {
                log.warn("HeaderVariableRequestBodyHandler joinPoint: {}, e: {}", joinPoint, e.getMessage());
            }
        }
    }
}
